package com.wzcl.app.config.oauth.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.wzcl.app.config.oauth.authentication.mobile.SmsCodeAuthenticationSecurityConfig;
import com.wzcl.app.config.oauth.authentication.mobile.SmsCodeFilter;
import com.wzcl.app.config.oauth.constants.FromLoginConstant;
import com.wzcl.app.dao.model.AppResource;
import com.wzcl.app.dao.model.AppRole;
import com.wzcl.app.model.dto.ResourceDto;
import com.wzcl.app.service.ResourceService;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.*;

@Log4j2
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

    private Map<String, List<ConfigAttribute>> resources = null;

    @Autowired
    private ResourceService resourceService;
    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
    @Autowired
    private DataSource dataSource;
    @Autowired
    private AuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler myAuthenticationFailureHandler;

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {

        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
        registry.requestMatchers(CorsUtils::isPreFlightRequest).permitAll();//让Spring security放行所有preflight request

        SmsCodeFilter smsCodeFilter = new SmsCodeFilter();
        smsCodeFilter.afterPropertiesSet();

        http.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
                // TODO 限制登录人数
                .authorizeRequests().anyRequest().authenticated().and()

                //表单登录,loginPage为登录请求的url,loginProcessingUrl为表单登录处理的URL
                .formLogin().loginProcessingUrl(FromLoginConstant.LOGIN_PROCESSING_URL)
                // TODO

                //登录成功之后的处理
                .successHandler(myAuthenticationSuccessHandler)
                .failureHandler(myAuthenticationFailureHandler)
                //允许访问
                .and()
                .authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
                        //自定义的权限决策器
                        fsi.setAccessDecisionManager(getAccessDecisionManager());
                        //自定义权限资源
                        fsi.setSecurityMetadataSource(getFilterInvocationSecurityMetadataSource());
                        return fsi;
                    }
                })
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(getAuthenticationEntryPoint())
                .accessDeniedHandler(getAccessDeniedHandler())
                .and().cors()
                //禁用跨站伪造
                .and().csrf().disable()
                //短信验证码配置
                .apply(smsCodeAuthenticationSecurityConfig);
        // 所有的rest服务一定要设置为无状态，以提升操作效率和性能
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

    }

    @Bean
    public AccessDecisionManager getAccessDecisionManager(){
        //权限决策器
        return new AccessDecisionManager(){
            /**
             *   取当前用户的权限与这次请求的这个url需要的权限作对比，决定是否放行
             *   * auth 包含了当前的用户信息，包括拥有的权限,即之前UserDetailsService登录时候存储的用户对象
             *   * object 就是FilterInvocation对象，可以得到request等web资源。
             *   * configAttributes 是本次访问需要的权限。
             */
            @Override
            public void decide(Authentication auth, Object o, Collection<ConfigAttribute> cas) throws AccessDeniedException, InsufficientAuthenticationException {
                if (cas.isEmpty()) {
                    return;
                }
                if (auth == null) {
                    throw new AccessDeniedException("权限不足!");
                }
                Iterator<ConfigAttribute> iterator = cas.iterator();
                while (iterator.hasNext()) {
                    ConfigAttribute ca = iterator.next();
                    //当前请求需要的权限
                    String needRole = ca.getAttribute();
                    //当前用户所具有的权限（决策权限校验）
                    for (GrantedAuthority authority : auth.getAuthorities()) {
                        if (authority.getAuthority().equals(needRole)) {
                            return;
                        }
                    }
                }
                throw new AccessDeniedException("用户权限不足!");
            }

            @Override
            public boolean supports(ConfigAttribute configAttribute) {
                return true;
            }

            @Override
            public boolean supports(Class<?> aClass) {
                return true;
            }
        };
    }


    @Bean
    public FilterInvocationSecurityMetadataSource getFilterInvocationSecurityMetadataSource(){
        return new FilterInvocationSecurityMetadataSource(){
            AntPathMatcher antPathMatcher = new AntPathMatcher();
            /**
             * 方法返回本次访问需要的权限
             * 返回为null:没有配置权限
             * 即默认都为白名单
             * @param object
             * @return
             * @throws IllegalArgumentException
             */
            @Override
            public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
                String requestUrl = ((FilterInvocation) object).getRequestUrl();
                //数据源
                Map<String, List<ConfigAttribute>> resources = getResources();
                for (Map.Entry<String, List<ConfigAttribute>> entry : resources.entrySet()) {
                    String uri = entry.getKey();
                    if (antPathMatcher.match(uri,requestUrl)) {
                        return entry.getValue();
                    }
                }
                return null;
            }

            /**
             * 方法如果返回了所有定义的权限资源
             * @return
             */
            @Override
            public Collection<ConfigAttribute> getAllConfigAttributes() {
                return null;
            }

            /**
             * 方法返回类对象是否支持校验
             * 一般使用FilterInvocation来判断，或者直接返回true
             * @param clazz
             * @return
             */
            @Override
            public boolean supports(Class<?> clazz) {
                return FilterInvocation.class.isAssignableFrom(clazz);
            }
        };
    }

    //初始化权限管理
    private Map<String, List<ConfigAttribute>> getResources() {
        if(resources == null){
            resources = new LinkedHashMap<>();
            //数据库获取资源
            List<AppResource> resourceList =  resourceService.getResourceList(new ResourceDto());
            if(resourceList != null && resourceList.size() > 0){
                for (AppResource appResource : resourceList) {
                    String uri = appResource.getName();
                    //对应权限
                    List<AppRole> roleList = appResource.getRoleList();
                    if (roleList != null && roleList.size() > 0) {
                        List<ConfigAttribute> attributes = new ArrayList<>();
                        for (AppRole appRole : roleList) {
                            ConfigAttribute auth = new SecurityConfig(appRole.getName());
                            attributes.add(auth);
                        }
                        resources.put(uri,attributes);
                    }
                }
            }
            log.info("初始化权限MAP配置");
        }
        return resources;
    }


    /*
        用来解决匿名用户访问无权限资源时的异常
     */
    @Bean
    public AuthenticationEntryPoint getAuthenticationEntryPoint() {
       return new AuthenticationEntryPoint() {
            private final ObjectMapper objectMapper = new ObjectMapper();
            @Override
            public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                Map<String,Object> map = new HashMap<>();
                map.put("code", 405);
                map.put("message", "禁止访问");
                map.put("path", request.getServletPath());
                response.setContentType("application/json;charset=UTF-8");
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.getWriter().write(objectMapper.writeValueAsString(map));
            }
        };
    }

    /*
    用来解决认证过的用户访问无权限资源时的异常
    */
    @Bean
    public AccessDeniedHandler getAccessDeniedHandler(){
        return new AccessDeniedHandler(){

            private final ObjectMapper objectMapper = new ObjectMapper();

            @Override
            public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
                Map<String,Object> map = new HashMap<>();
                map.put("code", 406);
                map.put("message", accessDeniedException.getMessage());
                map.put("path", request.getServletPath());
                response.setContentType("application/json;charset=UTF-8");
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.getWriter().write(objectMapper.writeValueAsString(map));
            }
        };
    }

    /*@Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.setAllowCredentials(true);
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(urlBasedCorsConfigurationSource);
    }*/

    @Bean
    public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher(){
        return new ServletListenerRegistrationBean<>(new HttpSessionEventPublisher());
    }

}
